package utils

import chisel3._
import chisel3.util.log2Ceil

import java.io.FileNotFoundException
import scala.collection.mutable
import scala.io.{BufferedSource, Source}

/**
 * 全局项目配置。每次编译的时候从磁盘读取。<br/>
 * 读取位置顺序：<br/>
 * 1. /build/.config: 当在 CI 模式下运行，将会把简化的配置文件 (.console.config) 复制到 /build/。<br/>
 * 2. /../.config:    本地调试的时候使用的文件，sbt。保证是最新配置。<br/>
 * 3. /.config:       本地调试的时候使用的文件，保证是最新配置。<br/>
 */
object GlobalConfigLoader {
  val debugReadingConfig = false

  abstract class ModuleOption
  (moduleConfigName: String, enable: Boolean = false)
  (implicit data: mutable.Map[String, Any]) {
    def enabled: Boolean = enable || data.contains(moduleConfigName)

    def getValue[T](use: () => T, default: T): T = {
      if (enabled) {
        try {
          use()
        } catch {
          case _: Throwable =>
            if (!enabled)
              logger.warn(s"Warning: get value fallback to $default")
            default
        }
      } else {
        logger.debug(s"$this use default value $default; enable = $enable, enabled = $enabled")
        default
      }
    }

    def getFullOptionName(optionName: String) = moduleConfigName + "_" + optionName

    def f(optionName: String) = getFullOptionName(optionName)

    def d[T](optionName: String): () => T = () => data(getFullOptionName(optionName)).asInstanceOf[T]

    def contains(optionName: String): Boolean = if (enabled) data.contains(getFullOptionName(optionName)) else false

    val inverseUpdateTable: Array[(String, Any)]

    def inverseUpdateAll(): Unit = data.addAll(inverseUpdateTable)
  }

  // val debug = true
  // Scala 的动态类型，可以动态设置成员内容
  trait GlobalConfig extends Dynamic {
    // private data: store Any type.
    implicit private val data: mutable.Map[String, Any] =
      mutable.HashMap.empty[String, Any].withDefault({ key => throw new NoSuchFieldError(key) })

    // get content
    def selectDynamic(key: String): Any = data(key)

    // set content
    def updateDynamic(key: String)(args: Any): Unit = {
      // Remove CONFIG_ header
      logger.debug(s"update: [$key = $args]")
      val keyUse: String = if (key.startsWith("CONFIG_")) key.substring("CONFIG_".length) else key
      if (debugReadingConfig && keyUse.startsWith("XM_")) logger.info(s"Setting $keyUse = $args")
      data(keyUse) = args
    }

    def has(key: String): Boolean = data.contains(key)

    object ask extends ModuleOption("XM_ASK", enable = true) {
      def clkPerBit = getValue(d("CLK_PER_BIT"), 60)

      def preCode = getValue(d("PRE_CODE"), "b11010101")

      def preCodeWidth = preCode.U.getWidth

      def lengthBit = getValue(d("LENGTH_BIT"), 8)

      def smallFirSize = 4

      def debug = false

      assert(smallFirSize - math.pow(2, log2Ceil(smallFirSize)).toInt == 0)

      abstract class User(name: String) extends ModuleOption(name, enable = true) {
        // TODO: auto test 8 width
        def sourceBit = getValue(d("SOURCE_BIT"), 8)

        def destBit = getValue(d("DEST_BIT"), 8)
      }

      object sender extends User("XM_ASK_SENDER") {
        def value0 = getValue(d("VALUE_0"), 0x00)

        def value1 = getValue(d("VALUE_1"), 0xff)

        def constantAdc = false

        def autoStart = false

        override val inverseUpdateTable = Array(
          f("SOURCE_BIT") -> sourceBit,
          f("DEST_BIT") -> destBit,
          f("VALUE_0") -> value0,
          f("VALUE_1") -> value1,
        )
      }

      object receiver extends User("XM_ASK_RECEIVER") {
        def threshold = getValue(d("THRESHOLD"), 191)

        def dacClkDiv = 0

        override val inverseUpdateTable = Array(
          f("SOURCE_BIT") -> sourceBit,
          f("DEST_BIT") -> destBit,
          f("THRESHOLD") -> threshold,
        )
      }

      override val inverseUpdateTable = Array(
        f("CLK_PER_BIT") -> clkPerBit,
        f("PRE_CODE") -> preCode,
        f("LENGTH_BIT") -> lengthBit,
      )
    }

    def namingWire: Boolean = data.contains("RUN_NAMING_WIRE") && data("RUN_NAMING_WIRE").asInstanceOf[Boolean]

    def updateFromData() = {}

    // invoke all
    List(ask)
  }

  object GlobalConfig {
    var synthMode = false
  }

  def generate: GlobalConfig = {
    // val resourceURI = ClassLoader.getSystemResource(".")
    val resourceURI = this.getClass.getClassLoader.getResource("/")
    // val resourceURI = new URL("file://./target/.")
    // logger.info(s"# resourceURI.toString = ${resourceURI}")
    val root = try {
      val resourcePath = resourceURI.getFile
      // /out/... 是 Mill 指定的编译目录，读取到的 resourcePath 在这下面；
      // /target/... 是 SBT 的编译目录，两者需要区别对待。
      val rootSplit = resourcePath.split(if (resourcePath.contains("/out/")) "/out/" else "/")
      assert(rootSplit.length == 2, s"Cannot locate root dir! resourcePath = $resourcePath")
      rootSplit(0)
    } catch {
      case _: NullPointerException =>
        // logger.info(s"resourceURI = $resourceURI")
        "."
    }
    logger.debug(s"config root dir: $root")

    def getConfigFile: Option[BufferedSource] = {
      try {
        Some(Source.fromFile(f"$root/build/.config"))
      } catch {
        case _: FileNotFoundException =>
          try {
            // may in .bloop
            Some(Source.fromFile(f"$root/../.config"))
          } catch {
            case _: FileNotFoundException => try
              Some(Source.fromFile(f"$root/.config"))
            catch {
              case _: FileNotFoundException => try {
                Some(Source.fromFile(f"$root/.console.config"))
              } catch {
                case _: FileNotFoundException =>
                  logger.info("No config file found! The default configuration value will be used!")
                  None
              }
            }
          }
      }
    }

    class GlobalConfigDefault extends GlobalConfig

    // 非空、非注释行
    val configFileOption = getConfigFile
    val c = new GlobalConfigDefault
    if (configFileOption.nonEmpty) {
      val configFile = configFileOption.get
      val configLines = configFile.getLines().filter(_.nonEmpty).filter(!_.startsWith("#"))
      assert(configLines.nonEmpty, "No config data!")
      configLines.foreach(line => {
        logger.debug(s"get c line: $line")
        val parsed = line.split("=")
        assert(parsed.length == 2, s"Config Error format: $line")
        val key = parsed(0)
        val valueString = parsed(1)
        if (valueString.startsWith("\"") && valueString.endsWith("\""))
          c.updateDynamic(key)(valueString.substring(1, valueString.length - 1))
        //  y 表示 true，false 则未定义。
        else if (valueString == "y") c.updateDynamic(key)(true)
        else try {
          // 优先转换 HEX
          if (valueString.startsWith("0x")) {
            c.updateDynamic(key)(Integer.parseInt(valueString.replace("0x", ""), 16))
          } else {
            val valueInt = valueString.toInt
            c.updateDynamic(key)(valueInt)
          }
        } catch {
          case _: NumberFormatException => c.updateDynamic(key)(valueString)
        }
      })
      c.updateFromData()
    }
    c
  }

  implicit val config: GlobalConfig = generate
}
